﻿//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Microsoft Public License.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace AstoriaOverAstoria
{
    using System;
    using System.Collections;
    using System.Data.Services.Providers;
    using System.Diagnostics;
    using System.Linq.Expressions;

    /// <summary>Converts property navigation expressions.</summary>
    internal class PropertyNavigationConverter : ExpressionConverter
    {
        /// <summary>Set if we have already found the outer most property navigation.</summary>
        private bool propertyNavigationFound;

        /// <summary>Constructor</summary>
        /// <param name="annotations">The resource annotations for the expressions being processed.</param>
        /// <param name="contextMapping">The context mapping, that it the metadata for the service this expression applies to.</param>
        public PropertyNavigationConverter(ExpressionResourceAnnotations annotations, MetadataMapping contextMapping)
            : base(annotations, contextMapping)
        {
            this.propertyNavigationFound = false;
        }

        /// <summary>Visits a method call expression.</summary>
        /// <param name="m">The method call to visit.</param>
        /// <returns>The visited expression.</returns>
        internal override Expression VisitMethodCall(MethodCallExpression m)
        {
            var selectMatch = ExpressionUtil.MatchSelectCall(m);
            if (selectMatch != null)
            {
                ResourceProperty property = this.Annotations.GetResourceProperty(selectMatch.LambdaBody);
                if (property != null && property.Kind == ResourcePropertyKind.ResourceReference && !this.propertyNavigationFound)
                {
                    // We project a single result (the value of the reference property)
                    // Astoria client will reuturn no results from such query if the reported result is null
                    //  (server returns 204, client turns that into empty enumerable).
                    // So if the result can be null, we need to post process the query and insert the null back in.
                    this.Result.AddQueryResultsProcessor(new ReturnNullIfEmptyQueryResultsProcessor());
                }

                // Select which project ExpandedWrapper or ProjectedWrapper is not a property navigation
                //   and thus ignore it, all other Selects are property navigations
                if (!TypeSystem.IsProjectedWrapperType(selectMatch.LambdaBody.Type) && !TypeSystem.IsExpandedWrapperType(selectMatch.LambdaBody.Type))
                {
                    this.propertyNavigationFound = true;
                }
            }

            var selectManyMatch = ExpressionUtil.MatchSelectManyCall(m);
            if (selectManyMatch != null)
            {
                this.propertyNavigationFound = true;
            }

            return base.VisitMethodCall(m);
        }

        /// <summary>Query results processor which returns a single null
        /// if the source results are empty.</summary>
        private class ReturnNullIfEmptyQueryResultsProcessor : IQueryResultsProcessor
        {
            /// <summary>Method which processes a single result of a query before it is handed to the server.</summary>
            /// <param name="result">The result produced by the query (and any result processors) so far.</param>
            /// <param name="context">The context which is executing the query.</param>
            /// <returns>The result of the query after the transformation.</returns>
            public object ProcessSingleResult(object result, AstoriaOverAstoriaContext context)
            {
                throw new NotSupportedException("ReturnNullIfEmptyQueryResultsProcessor should never be used for a single result queries.");
            }

            /// <summary>Method which processes the results of a query before they are handed to the server.</summary>
            /// <param name="results">The results of the query so far.</param>
            /// <param name="context">The context which is executing the query.</param>
            /// <returns>The results of the query after the transformation.</returns>
            public IEnumerable ProcessResults(IEnumerable results, AstoriaOverAstoriaContext context)
            {
                int count = 0;
                foreach (object item in results)
                {
                    count++;
                    yield return item;
                }

                if (count == 0)
                {
                    yield return null;
                }
            }
        }
    }
}
